(cl:defpackage #:abtab-delimited-driver
  (:use #:common-lisp #:abtab #:cl-ppcre #:krb-util)
  (:export
   #:abtab-delimited-driver
   #:format-delimited-line
   #:parse-line
   #:format-line
   #:parse-delimited-line
   ))

(in-package #:abtab-delimited-driver)

;; it is ideal to have the ability to have the file based drivers be
;; able to perform the following behaviors:
;;  - be given a stream at construction time (especially for mocking out tests)
;;  - use a file path and truncate, append, and read from the file
;;    - this means holding open a file handle
;;    - or it means mem-mapping the file (preferable for performance)
;;
;; In the first case though, both reading from and writing to is more
;; difficult if the stream can't itself be reset...

(defclass abtab-delimited-driver (abtab-driver)
  ((record-delimiter
    :accessor record-delimiter
    :initform "\\r?\\n"
    :initarg :rsep
    :documentation "The record delimiter, a regular expression.")
   (field-delimiter
    :accessor field-delimiter
    :initform "\\t"
    :initarg :delim
    :documentation "The field delimiter, a regular expression.")
   (escape-character
    :accessor escape-character
    :initform "\\"
    :initarg :escape-char
    :documentation "The escape seqeunce for escaping the field-delimiter in the stream.  Two instances of this are necessary to specify the character itself.  This value may be a string and not just a character as the name implies.")
   (port
    :accessor port
    :initform nil
    :initarg :port
    :documentation "Slot to hold the port, if null when reset-for-reading or initialize-destination are called, the file specified by the path portion of the uri will be opened.")
   (has-header
    :accessor has-header
    :initform t
    :initarg :has-header
    :documentation "Whether to pull the column info from the first record, defaults to true."))
  (:documentation "A delimited stream handler - this is not suitable
   for a CSV parser, which needs a state machine to read the stream.
   This parser uses cl-ppcre:split to parse line entries into
   records.  For a CSV suitable driver, see the class abtab-csv-driver."))

;; seek => (file-position stream absolute-position)

(defun parse-line (delim escape nfields str)
  (declare (ignore escape))
  (ensure-length nfields (->vector (cl-ppcre:split delim str))))

(defgeneric parse-delimited-line (driver string))

(defmethod parse-delimited-line ((drv abtab-delimited-driver)
				 (str simple-array))
  (parse-line
   (field-delimiter drv)
   (escape-character drv)
   (length (columns drv))
   str))

(defun format-line (delim escape nfields rec)
  ;; ignore for now, but escape should be consulted when ->string'ing
  ;; the fields themselves...call out to cl-ppcre to search/replace
  (declare (ignore escape))
  (if (> (length rec)
	 nfields)
      (error 'record-too-long :text (format nil "expected ~a fields, record had ~a fields" nfields (length rec))))
  (loop
     for ii from 1 upto (1- nfields)
     with str = (aif (= 0 (length rec))
		     ""
		     (->string (aref rec 0)))
     finally (return str)
     do
     (if (> (length rec) ii)
	 (setf str (concatenate 'string str delim (->string (aref rec ii))))
	 (setf str (concatenate 'string str delim)))))

(defgeneric format-delimited-line (driver record))

(defmethod format-delimited-line ((drv abtab-delimited-driver)
				 (rec simple-vector))
  (format-line
   (field-delimiter drv)
   (escape-character drv)
   (length (columns drv))
   rec))



(defmethod format-delimited-line ((drv abtab-delimited-driver)
				  (rec list))
  (format-delimited-line drv (->vector rec)))



